import socket, time, os, random, string, sys
from threading import Thread, Lock
from Package import *
from Protocol import *
from xml.dom import minidom 

class Server:

  ## data objects (for clearance all class variables are shown here)
  debug = None
  # OOP-related
  protocol   = None # instance of protocol class
  socket     = None # We need just one for UDP
  tag        = None
 
  mutes_parent    = None
  mutex_children  = None
  mutex_leaves    = None
  mutex_responses = None
  mutex_servers   = None
  mutes_clients   = None

  # network-related
  parent   = None   # ID, ip, port
  children = None   # For each server: ID, ip, port
  leaves   = None   # Connected clients with THIS server
  servers  = None   # (Additional servers) for each server: ID, closest.server
  clients  = None   # connected clients with all servers
  
  responses = None  # expected responses from clients/server/main server
  
  # constructor
  def __init__(self):
    self.parent    = None
    self.admins    = {}
    self.children  = []
    self.leaves    = []
    self.clients   = []
    self.responses = [] # wat/van wie/tot wanneer
    
    self.running = False # wordt gebruikt om de ping_thread netjes te stoppen als de main thread stopt.
    
    self.mutex_parent    = Lock()
    self.mutex_children  = Lock()
    self.mutex_leaves    = Lock()
    self.mutex_responses = Lock()
    self.mutex_servers   = Lock()
    self.mutex_clients   = Lock()
    
    self.mutex_printdb = Lock()

    self.import_settings_from_file()
    self.socket_setup()
    
    self.protocol = Protocol(self)
   
    self.generate_unique_tag()
   
    self.printdb("Connecting to control server")
    self.protocol.hello_to_control_601( self.central_server['ip:port'], self.tag)
    
    self.printdb("Starting ping and responses thread")
    self.running = True
    
    self.ping_thread = Thread(target = self.ping_loop)
    self.ping_thread.start()
    
    self.responses_thread = Thread(target = self.control_responses)
    self.responses_thread.start()
    
    self.printdb("Starting main loop")
    
    try:
      self.loop()
    except (KeyboardInterrupt, SystemExit):
      self.stop()


  def import_settings_from_file(self): 
    doc = minidom.parse("settings.xml")

    logging = doc.getElementsByTagName("logging")[0]

    self.debug = self.get_text_element(logging, "debug") == "true"
    self.log_path = self.get_text_element(logging, "path")
    
    if not self.debug:
      self.logfile = open(self.log_path + str(time.time()) + ".txt", "w")
      sys.stderr = self.logfile
   
    self.printdb("Starting Chat Server")
    self.printdb(time.ctime(time.time()) + '\n')
    self.printdb("Importing settings from settings.xml...")
    
    administrators = doc.getElementsByTagName("administrator")
    for administrator in administrators:
      username = self.get_text_element(administrator, "username")
      password = self.get_text_element(administrator, "password")
      self.admins[username] = password
    self.printdb("Found " + str(len(self.admins)) + " administrators")
    
    connection_settings = doc.getElementsByTagName("connection")[0]
    self.port = int(self.get_text_element(connection_settings, "port"))
    self.host = self.get_text_element(connection_settings, "host")
    self.printdb("Using IP:   " + str(self.host))
    self.printdb("Using Port: " + str(self.port))
    
    cserver_settings = doc.getElementsByTagName("centralserver")[0]
    ip = self.get_text_element(cserver_settings, "address")
    port = int(self.get_text_element(cserver_settings, "port"))
    self.central_server = {'ip': ip, 'volgnummer': 0, 'ip:port':(ip, port)}
    self.printdb("Using controlserver at: " + str(self.central_server['ip:port']))
      
  # Searches a child of root with given name and returns its text value
  def get_text_element(self, root, name):
    return root.getElementsByTagName(name)[0].firstChild.data.strip()
  
  # debug print statement (define debug in settings.xml)
  def printdb(self, message):
    self.mutex_printdb.acquire()
    if self.debug:
      print message
    else:
      self.logfile.write(message + "\n")
      self.logfile.flush()
    self.mutex_printdb.release()
  
  # setup UDP socket
  def socket_setup(self):
    self.printdb("Initializing socket\n")
    self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)    

  # control thread: check if expected packages have arrived
  def control_responses(self):
    self.printdb("Control thread started")
    while self.running:
      # loop door lijst met verwachte responses
      # als houdbaarheid voorbij, doe een gepaste actie
      # note: wat een gepaste actie is hangt van de situatie af,
      # maar in bijna alle gevallen het sturen van een package dat
      # in het huidige protocol niet is gedefinieerd.
      self.mutex_responses.acquire()
      for i in range(0, len(self.responses)):
        pass  # not implemented. see report.
      self.mutex_responses.release()
    
      # schedule other thread
      time.sleep(0)
    self.printdb("Responses thread exited")

  # ping thread: ping inactive clients and servers.
  # disconnect when no response on the ping has been received
  def ping_loop(self):
    self.printdb("Ping thread started")
    while self.running:
      now = time.time()
      everything = self.protocol.get_everything()
      for node in everything:
        if (node['last_updated']):

          # no ping received (30 + 2 sec):
          if (now - node['last_updated'] > 32):
            if ('name' in node):
              self.printdb("Client: " + node['name'] + " 32 seconds inactive.")
              self.mutex_leaves.acquire()
              self.leaves.remove(node)
              self.mutex_leaves.release()
              self.printdb("Disconnecting client: " + node['name'])
              for send_node in everything:
                if (send_node != node):
                  self.protocol.removed_client_130(send_node['ip:port'], node['name'], "Client not responding")
            else:
              self.printdb("Server: " + str(node['ip:port']) + " 32 seconds inactive.")
              is_parent = False
              self.mutex_parent.acquire()
              if (node == self.parent):
                is_parent = True
                self.parent = None                
              self.mutex_parent.release()
              if (not is_parent):
                self.mutex_children.acquire()
                self.children.remove(node)            
                self.mutex_children.release()

              self.protocol.peer_server_disconnected_603(self.central_server['ip:port'], node['tag'])

          # inactive for 30 sec: send ping
          elif (now - node['last_updated'] > 30):
            self.printdb("Node: " + str(node['ip:port']) + " 30 seconds inactive.")
            self.protocol.ping_140(node['ip:port'])

      time.sleep(0.5)
    self.printdb("Ping thread exited")

  # loop: listen on socket, waiting for new packages
  def loop(self):
    while  True:
      self.printdb("Waiting for a new package")
      raw_package, addr = self.socket.recvfrom(200)
      package = Package.from_binary(raw_package)
      self.printdb("Received a new package from: " + str(addr))
      self.printdb("Starting new thread to handle the received " + str(package.code) + " package")
      # new package received: start thread to handle
      # and (in this thread) go on listening for even more packages
      thread = Thread(target = self.protocol.handle_input, args = (package, addr))
      thread.start()

  def find_connected_node_by_addr(self, addr):     
    everything = self.protocol.get_everything()
    for node in everything:
      if (node['ip:port']):
        if (node['ip:port'] == addr):
          return node

  def generate_unique_tag(self):
    self.printdb("Generating unique tag")
    pid    = os.getpid()
    length = 28 - len(str(pid))
    temp   = ''
    random.seed(time.time() * self.port * pid)
    for i in range(length):
      temp += random.choice(string.letters + string.digits)
    self.tag = "T04S" + str(pid) + temp
    self.printdb("Generated tag: " + self.tag)

  def stop(self):
    self.printdb("Gracefully terminating all threads before quitting")
    self.running = False
    self.ping_thread.join()  
    self.responses_thread.join()
    
    for leaf in self.leaves:
      self.protocol.quit_701(leaf['ip:port'], "Server is disconnecting")
    
    self.printdb("Bye bye!")
    
    if not self.debug:
      self.logfile.close()

